fix: migrate Gemini Interactions emitter to SDK 2.x event protocol (#277)#279
Open
Conversation
The Interactions mock emitter produced the SDK 1.x streamed event shapes (interaction.start/complete, content.start/delta/stop), which have zero event_type overlap with the SDK 2.x adapter (@google/genai v2 'Interactions breaking changes, May 2026'). A v2 consumer hit its switch default for every event and rendered an empty assistant message. - Rewrite the three SSE builders to emit interaction.created/completed and step.start/delta/stop. Tool-call identity (id/name) now lives on step.start with an empty arguments placeholder; arguments stream as a dedicated arguments_delta carrying a JSON-string fragment (valid JSON by step.stop). - Count step.delta (not content.delta) for truncateAfterChunks budget. - Teach collapseGeminiInteractionsSSE the 2.x shapes (step.start identity + arguments_delta assembly, nested thought_summary), keeping 1.x parsing for backward compatibility with previously recorded fixtures. - Update unit/integration/collapse tests and drift SDK shapes to 2.x; add round-trip and legacy backward-compat coverage. Closes CopilotKit#277
Address review findings on the SDK 2.x migration: - Flag assembled arguments_delta fragments that don't concatenate into valid JSON by step.stop via droppedChunks/firstDroppedSample, instead of silently writing a corrupt tool call into a recorded fixture. - Preserve the identity of a function_call step.start that arrives without an index by minting a synthetic key (matches the sibling collapsers) rather than dropping it silently. - Reword the emitter's arguments_delta comment: the mock emits one whole fragment (a valid degenerate case), it does not itself concatenate. - Add tests: multiple/interleaved tool calls collapse in step-index order, emitter assigns sequential step indices, invalid-JSON assembly is flagged, and an index-less function_call step.start keeps its identity.
- Streaming builders: malformed tool-call arguments degrade to a valid '{}'
arguments_delta fragment and emit a warning.
- Tool-call and content+tools streams: assert usage propagates and the
terminal status is requires_action on interaction.completed (unit + e2e).
- Collapser: pin the ordering behavior when an arguments_delta arrives before
its step.start — the early fragment is flagged via droppedChunks rather than
mis-attributed, and the call still surfaces with empty args.
commit: |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Migrates the Gemini Interactions mock emitter from the SDK 1.x streamed-event format to SDK 2.x (the "Interactions breaking changes, May 2026" shapes in
@google/genaiv2). Previously the emitter producedinteraction.start/interaction.complete+content.start/content.delta/content.stop, which have zeroevent_typeoverlap with the v2 adapter — a migrated consumer hit itsswitchdefault for every event and rendered an empty assistant message.Closes #277.
What changed
src/gemini-interactions.ts— rewrote the three SSE builders:interaction.start/complete→interaction.created/completed(id stays populated on both; status text→completed, tool calls→requires_action).content.start/delta/stop→step.start { step: { type: "model_output" } }/step.delta { delta: { type: "text", text } }/step.stop.id,name) now lives onstep.startwith anarguments: {}placeholder; arguments stream asstep.delta { delta: { type: "arguments_delta", arguments: "<json-string fragment>" } }, valid JSON bystep.stop.writeGeminiInteractionsSSEStreamnow countsstep.delta(notcontent.delta) for thetruncateAfterChunksbudget.src/stream-collapse.ts—collapseGeminiInteractionsSSEnow parses 2.x events (assemble tool calls fromstep.startidentity +arguments_deltastring fragments keyed by index; nestedthought_summary.content.text), while keeping 1.x parsing for previously recorded fixtures. Hardened against silent data loss: malformed assembled args and index-less / uncorrelatedstep.start/arguments_deltaare flagged viadroppedChunks/firstDroppedSamplerather than written silently.Tests + drift baseline — updated unit/integration/collapse tests and
src/__tests__/drift/sdk-shapes.tsto 2.x; added round-trip, multiple/interleaved tool calls, ordering, malformed-args, usage/status, and 1.x backward-compat coverage.Verification
grep -E 'event_type: "(step|interaction\.created|interaction\.completed)' dist/gemini-interactions.jsmatches after build (20 hits, 0 legacy shapes remain). Full suite: 4026 passing, prettier + eslint clean.
This unblocks re-enabling the
stateful-interactionse2e test in TanStack/ai#781 once aimock is bumped.Notes / scope
outputs: [{ type: "function_call", ... }]) were intentionally left unchanged — the issue scoped the wire-format mismatch to the streamed SSE emitter.function_callwhosestep.startcarries an empty{}placeholder but never receives anarguments_deltafinalizes to"{}". On the wire that is byte-identical to a legitimately empty-args call, so flagging it would false-positive on every legitimate empty-args call.🤖 Generated with Claude Code